/*
 * Copyright 2010 Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package sif.FacebookSDK;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;

import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieSyncManager;

/**
 * Main Facebook object for interacting with the Facebook developer API.
 * Provides methods to log in and log out a user, make requests using the REST
 * and Graph APIs, and start user interface interactions with the API (such as
 * pop-ups promoting for credentials, permissions, stream posts, etc.)
 * 
 * @author Jim Brusstar (jimbru@facebook.com), Yariv Sadan (yariv@facebook.com),
 *         Luke Shepard (lshepard@facebook.com)
 */
public class Facebook {

	// Strings used in the authorization flow
	public static final String REDIRECT_URI = "fbconnect://success";
	public static final String CANCEL_URI = "fbconnect://cancel";
	public static final String TOKEN = "access_token";
	public static final String EXPIRES = "expires_in";
	public static final String SINGLE_SIGN_ON_DISABLED = "service_disabled";

	public static final int FORCE_DIALOG_AUTH = -1;

	private static final String LOGIN = "oauth";

	// Used as default activityCode by authorize(). See authorize() below.
	private static final int DEFAULT_AUTH_ACTIVITY_CODE = 32665;

	// Facebook server endpoints: may be modified in a subclass for testing
	protected static String DIALOG_BASE_URL = "https://m.facebook.com/dialog/";
	protected static String GRAPH_BASE_URL = "https://graph.facebook.com/";
	protected static String RESTSERVER_URL = "https://api.facebook.com/restserver.php";

	private String mAccessToken = null;
	private long mLastAccessUpdate = 0;
	private long mAccessExpires = 0;
	private String mAppId;

	private Activity mAuthActivity;
	private String[] mAuthPermissions;
	private int mAuthActivityCode;
	private DialogListener mAuthDialogListener;

	// If the last time we extended the access token was more than 24 hours ago
	// we try to refresh the access token again.
	final private long REFRESH_TOKEN_BARRIER = 24L * 60L * 60L * 1000L;

	/**
	 * Constructor for Facebook object.
	 * 
	 * @param appId
	 *            Your Facebook application ID. Found at
	 *            www.facebook.com/developers/apps.php.
	 */
	public Facebook(String appId) {
		if (appId == null) {
			throw new IllegalArgumentException(
					"You must specify your application ID when instantiating "
							+ "a Facebook object. See README for details.");
		}
		mAppId = appId;
	}

	/**
	 * Default authorize method. Grants only basic permissions.
	 * 
	 * See authorize() below for @params.
	 */
	public void authorize(Activity activity, final DialogListener listener) {
//		authorize(activity, new String[] {}, DEFAULT_AUTH_ACTIVITY_CODE,
//				listener);
		authorize(activity, new String[] {}, Facebook.FORCE_DIALOG_AUTH,
				listener);
	}

	/**
	 * Authorize method that grants custom permissions.
	 * 
	 * See authorize() below for @params.
	 */
	public void authorize(Activity activity, String[] permissions,
			final DialogListener listener) {
//		authorize(activity, permissions, DEFAULT_AUTH_ACTIVITY_CODE, listener);
		authorize(activity, permissions, Facebook.FORCE_DIALOG_AUTH, listener);
	}

	/**
	 * Full authorize method.
	 * 
	 * Starts either an Activity or a dialog which prompts the user to log in to
	 * Facebook and grant the requested permissions to the given application.
	 * 
	 * This method will, when possible, use Facebook's single sign-on for
	 * Android to obtain an access token. This involves proxying a call through
	 * the Facebook for Android stand-alone application, which will handle the
	 * authentication flow, and return an OAuth access token for making API
	 * calls.
	 * 
	 * Because this process will not be available for all users, if single
	 * sign-on is not possible, this method will automatically fall back to the
	 * OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled
	 * by Facebook in an embedded WebView, not by the client application. As
	 * such, the dialog makes a network request and renders HTML content rather
	 * than a native UI. The access token is retrieved from a redirect to a
	 * special URL that the WebView handles.
	 * 
	 * Note that User credentials could be handled natively using the OAuth 2.0
	 * Username and Password Flow, but this is not supported by this SDK.
	 * 
	 * See http://developers.facebook.com/docs/authentication/ and
	 * http://wiki.oauth.net/OAuth-2 for more details.
	 * 
	 * Note that this method is asynchronous and the callback will be invoked in
	 * the original calling thread (not in a background thread).
	 * 
	 * Also note that requests may be made to the API without calling authorize
	 * first, in which case only public information is returned.
	 * 
	 * IMPORTANT: Note that single sign-on authentication will not function
	 * correctly if you do not include a call to the authorizeCallback() method
	 * in your onActivityResult() function! Please see below for more
	 * information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH
	 * as the activityCode parameter in your call to authorize().
	 * 
	 * @param activity
	 *            The Android activity in which we want to display the
	 *            authorization dialog.
	 * @param applicationId
	 *            The Facebook application identifier e.g. "350685531728"
	 * @param permissions
	 *            A list of permissions required for this application: e.g.
	 *            "read_stream", "publish_stream", "offline_access", etc. see
	 *            http://developers.facebook.com/docs/authentication/permissions
	 *            This parameter should not be null -- if you do not require any
	 *            permissions, then pass in an empty String array.
	 * @param activityCode
	 *            Single sign-on requires an activity result to be called back
	 *            to the client application -- if you are waiting on other
	 *            activities to return data, pass a custom activity code here to
	 *            avoid collisions. If you would like to force the use of legacy
	 *            dialog-based authorization, pass FORCE_DIALOG_AUTH for this
	 *            parameter. Otherwise just omit this parameter and Facebook
	 *            will use a suitable default. See
	 *            http://developer.android.com/reference/android/
	 *            app/Activity.html for more information.
	 * @param listener
	 *            Callback interface for notifying the calling application when
	 *            the authentication dialog has completed, failed, or been
	 *            canceled.
	 */
	public void authorize(Activity activity, String[] permissions,
			int activityCode, final DialogListener listener) {

		boolean singleSignOnStarted = false;

		mAuthDialogListener = listener;

		// Prefer single sign-on, where available.
		if (activityCode >= 0) {
			singleSignOnStarted = startSingleSignOn(activity, mAppId,
					permissions, activityCode);
		}
		// Otherwise fall back to traditional dialog.
		if (!singleSignOnStarted) {
			startDialogAuth(activity, permissions);
		}
	}

	/**
	 * Internal method to handle single sign-on backend for authorize().
	 * 
	 * @param activity
	 *            The Android Activity that will parent the ProxyAuth Activity.
	 * @param applicationId
	 *            The Facebook application identifier.
	 * @param permissions
	 *            A list of permissions required for this application. If you do
	 *            not require any permissions, pass an empty String array.
	 * @param activityCode
	 *            Activity code to uniquely identify the result Intent in the
	 *            callback.
	 */
	private boolean startSingleSignOn(Activity activity, String applicationId,
			String[] permissions, int activityCode) {
		boolean didSucceed = true;
		Intent intent = new Intent();

		intent.setClassName("com.facebook.katana",
				"com.facebook.katana.ProxyAuth");
		intent.putExtra("client_id", applicationId);
		if (permissions.length > 0) {
			intent.putExtra("scope", TextUtils.join(",", permissions));
		}

		// Verify that the application whose package name is
		// com.facebook.katana.ProxyAuth
		// has the expected FB app signature.
		if (!validateAppSignatureForIntent(activity, intent)) {
			return false;
		}

		mAuthActivity = activity;
		mAuthPermissions = permissions;
		mAuthActivityCode = activityCode;
		try {
			activity.startActivityForResult(intent, activityCode);
		} catch (ActivityNotFoundException e) {
			didSucceed = false;
		}

		return didSucceed;
	}

	/**
	 * Query the signature for the application that would be invoked by the
	 * given intent and verify that it matches the FB application's signature.
	 * 
	 * @param context
	 * @param intent
	 * @param validSignature
	 * @return true if the app's signature matches the expected signature.
	 */
	private boolean validateAppSignatureForIntent(Context context, Intent intent) {

		ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(
				intent, 0);
		if (resolveInfo == null) {
			return false;
		}

		String packageName = resolveInfo.activityInfo.packageName;
		PackageInfo packageInfo;
		try {
			packageInfo = context.getPackageManager().getPackageInfo(
					packageName, PackageManager.GET_SIGNATURES);
		} catch (NameNotFoundException e) {
			return false;
		}

		for (Signature signature : packageInfo.signatures) {
			if (signature.toCharsString().equals(FB_APP_SIGNATURE)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Internal method to handle dialog-based authentication backend for
	 * authorize().
	 * 
	 * @param activity
	 *            The Android Activity that will parent the auth dialog.
	 * @param applicationId
	 *            The Facebook application identifier.
	 * @param permissions
	 *            A list of permissions required for this application. If you do
	 *            not require any permissions, pass an empty String array.
	 */
	private void startDialogAuth(Activity activity, String[] permissions) {
		Bundle params = new Bundle();
		if (permissions.length > 0) {
			params.putString("scope", TextUtils.join(",", permissions));
		}
		CookieSyncManager.createInstance(activity);
		dialog(activity, LOGIN, params, new DialogListener() {

			public void onComplete(Bundle values) {
				// ensure any cookies set by the dialog are saved
				CookieSyncManager.getInstance().sync();
				setAccessToken(values.getString(TOKEN));
				setAccessExpiresIn(values.getString(EXPIRES));
				if (isSessionValid()) {
					Log.d("Facebook-authorize", "Login Success! access_token="
							+ getAccessToken() + " expires="
							+ getAccessExpires());
					mAuthDialogListener.onComplete(values);
				} else {
					mAuthDialogListener.onFacebookError(new FacebookError(
							"Failed to receive access token."));
				}
			}

			public void onError(DialogError error) {
				Log.d("Facebook-authorize", "Login failed: " + error);
				mAuthDialogListener.onError(error);
			}

			public void onFacebookError(FacebookError error) {
				Log.d("Facebook-authorize", "Login failed: " + error);
				mAuthDialogListener.onFacebookError(error);
			}

			public void onCancel() {
				Log.d("Facebook-authorize", "Login canceled");
				mAuthDialogListener.onCancel();
			}
		});
	}

	/**
	 * IMPORTANT: This method must be invoked at the top of the calling
	 * activity's onActivityResult() function or Facebook authentication will
	 * not function properly!
	 * 
	 * If your calling activity does not currently implement onActivityResult(),
	 * you must implement it and include a call to this method if you intend to
	 * use the authorize() method in this SDK.
	 * 
	 * For more information, see
	 * http://developer.android.com/reference/android/app/
	 * Activity.html#onActivityResult(int, int, android.content.Intent)
	 */
	public void authorizeCallback(int requestCode, int resultCode, Intent data) {
		if (requestCode == mAuthActivityCode) {

			// Successfully redirected.
			if (resultCode == Activity.RESULT_OK) {

				// Check OAuth 2.0/2.10 error code.
				String error = data.getStringExtra("error");
				if (error == null) {
					error = data.getStringExtra("error_type");
				}

				// A Facebook error occurred.
				if (error != null) {
					if (error.equals(SINGLE_SIGN_ON_DISABLED)
							|| error.equals("AndroidAuthKillSwitchException")) {
						Log.d("Facebook-authorize", "Hosted auth currently "
								+ "disabled. Retrying dialog auth...");
						startDialogAuth(mAuthActivity, mAuthPermissions);
					} else if (error.equals("access_denied")
							|| error.equals("OAuthAccessDeniedException")) {
						Log.d("Facebook-authorize", "Login canceled by user.");
						mAuthDialogListener.onCancel();
					} else {
						String description = data
								.getStringExtra("error_description");
						if (description != null) {
							error = error + ":" + description;
						}
						Log.d("Facebook-authorize", "Login failed: " + error);
						mAuthDialogListener.onFacebookError(new FacebookError(
								error));
					}

					// No errors.
				} else {
					setAccessToken(data.getStringExtra(TOKEN));
					setAccessExpiresIn(data.getStringExtra(EXPIRES));
					if (isSessionValid()) {
						Log.d("Facebook-authorize",
								"Login Success! access_token="
										+ getAccessToken() + " expires="
										+ getAccessExpires());
						mAuthDialogListener.onComplete(data.getExtras());
					} else {
						mAuthDialogListener.onFacebookError(new FacebookError(
								"Failed to receive access token."));
					}
				}

				// An error occurred before we could be redirected.
			} else if (resultCode == Activity.RESULT_CANCELED) {

				// An Android error occured.
				if (data != null) {
					Log.d("Facebook-authorize",
							"Login failed: " + data.getStringExtra("error"));
					mAuthDialogListener.onError(new DialogError(data
							.getStringExtra("error"), data.getIntExtra(
							"error_code", -1), data
							.getStringExtra("failing_url")));

					// User pressed the 'back' button.
				} else {
					Log.d("Facebook-authorize", "Login canceled by user.");
					mAuthDialogListener.onCancel();
				}
			}
		}
	}

	/**
	 * Refresh OAuth access token method. Binds to Facebook for Android
	 * stand-alone application application to refresh the access token. This
	 * method tries to connect to the Facebook App which will handle the
	 * authentication flow, and return a new OAuth access token. This method
	 * will automatically replace the old token with a new one. Note that this
	 * method is asynchronous and the callback will be invoked in the original
	 * calling thread (not in a background thread).
	 * 
	 * @param context
	 *            The Android Context that will be used to bind to the Facebook
	 *            RefreshToken Service
	 * @param serviceListener
	 *            Callback interface for notifying the calling application when
	 *            the refresh request has completed or failed (can be null). In
	 *            case of a success a new token can be found inside the result
	 *            Bundle under Facebook.ACCESS_TOKEN key.
	 * @return true if the binding to the RefreshToken Service was created
	 */
	public boolean extendAccessToken(Context context,
			ServiceListener serviceListener) {
		Intent intent = new Intent();

		intent.setClassName("com.facebook.katana",
				"com.facebook.katana.platform.TokenRefreshService");

		// Verify that the application whose package name is
		// com.facebook.katana
		// has the expected FB app signature.
		if (!validateAppSignatureForIntent(context, intent)) {
			return false;
		}

		return context.bindService(intent, new TokenRefreshServiceConnection(
				context, serviceListener), Context.BIND_AUTO_CREATE);
	}

	/**
	 * Calls extendAccessToken if shouldExtendAccessToken returns true.
	 * 
	 * @return the same value as extendAccessToken if the the token requires
	 *         refreshing, true otherwise
	 */
	public boolean extendAccessTokenIfNeeded(Context context,
			ServiceListener serviceListener) {
		if (shouldExtendAccessToken()) {
			return extendAccessToken(context, serviceListener);
		}
		return true;
	}

	/**
	 * Check if the access token requires refreshing.
	 * 
	 * @return true if the last time a new token was obtained was over 24 hours
	 *         ago.
	 */
	public boolean shouldExtendAccessToken() {
		return isSessionValid()
				&& (System.currentTimeMillis() - mLastAccessUpdate >= REFRESH_TOKEN_BARRIER);
	}

	/**
	 * Handles connection to the token refresh service (this service is a part
	 * of Facebook App).
	 */
	private class TokenRefreshServiceConnection implements ServiceConnection {

		final Messenger messageReceiver = new Messenger(new Handler() {
			@Override
			public void handleMessage(Message msg) {
				String token = msg.getData().getString(TOKEN);
				long expiresAt = msg.getData().getLong(EXPIRES) * 1000L;

				// To avoid confusion we should return the expiration time in
				// the same format as the getAccessExpires() function - that
				// is in milliseconds.
				Bundle resultBundle = (Bundle) msg.getData().clone();
				resultBundle.putLong(EXPIRES, expiresAt);

				if (token != null) {
					setAccessToken(token);
					setAccessExpires(expiresAt);
					if (serviceListener != null) {
						serviceListener.onComplete(resultBundle);
					}
				} else if (serviceListener != null) { // extract errors only if
														// client wants them
					String error = msg.getData().getString("error");
					if (msg.getData().containsKey("error_code")) {
						int errorCode = msg.getData().getInt("error_code");
						serviceListener.onFacebookError(new FacebookError(
								error, null, errorCode));
					} else {
						serviceListener.onError(new Error(error != null ? error
								: "Unknown service error"));
					}
				}

				// The refreshToken function should be called rarely,
				// so there is no point in keeping the binding open.
				applicationsContext
						.unbindService(TokenRefreshServiceConnection.this);
			}
		});

		final ServiceListener serviceListener;
		final Context applicationsContext;

		Messenger messageSender = null;

		public TokenRefreshServiceConnection(Context applicationsContext,
				ServiceListener serviceListener) {
			this.applicationsContext = applicationsContext;
			this.serviceListener = serviceListener;
		}

		@Override
		public void onServiceConnected(ComponentName className, IBinder service) {
			messageSender = new Messenger(service);
			refreshToken();
		}

		@Override
		public void onServiceDisconnected(ComponentName arg) {
			serviceListener.onError(new Error("Service disconnected"));
			// We returned an error so there's no point in
			// keeping the binding open.
			mAuthActivity.unbindService(TokenRefreshServiceConnection.this);
		}

		private void refreshToken() {
			Bundle requestData = new Bundle();
			requestData.putString(TOKEN, mAccessToken);

			Message request = Message.obtain();
			request.setData(requestData);
			request.replyTo = messageReceiver;

			try {
				messageSender.send(request);
			} catch (RemoteException e) {
				serviceListener.onError(new Error("Service connection error"));
			}
		}
	};

	/**
	 * Invalidate the current user session by removing the access token in
	 * memory, clearing the browser cookie, and calling auth.expireSession
	 * through the API.
	 * 
	 * Note that this method blocks waiting for a network response, so do not
	 * call it in a UI thread.
	 * 
	 * @param context
	 *            The Android context in which the logout should be called: it
	 *            should be the same context in which the login occurred in
	 *            order to clear any stored cookies
	 * @throws IOException
	 * @throws MalformedURLException
	 * @return JSON string representation of the auth.expireSession response
	 *         ("true" if successful)
	 */
	public String logout(Context context) throws MalformedURLException,
			IOException {
		Util.clearCookies(context);
		Bundle b = new Bundle();
		b.putString("method", "auth.expireSession");
		String response = request(b);
		setAccessToken(null);
		setAccessExpires(0);
		return response;
	}

	/**
	 * Make a request to Facebook's old (pre-graph) API with the given
	 * parameters. One of the parameter keys must be "method" and its value
	 * should be a valid REST server API method.
	 * 
	 * See http://developers.facebook.com/docs/reference/rest/
	 * 
	 * Note that this method blocks waiting for a network response, so do not
	 * call it in a UI thread.
	 * 
	 * Example: <code>
	 *  Bundle parameters = new Bundle();
	 *  parameters.putString("method", "auth.expireSession");
	 *  String response = request(parameters);
	 * </code>
	 * 
	 * @param parameters
	 *            Key-value pairs of parameters to the request. Refer to the
	 *            documentation: one of the parameters must be "method".
	 * @throws IOException
	 *             if a network error occurs
	 * @throws MalformedURLException
	 *             if accessing an invalid endpoint
	 * @throws IllegalArgumentException
	 *             if one of the parameters is not "method"
	 * @return JSON string representation of the response
	 */
	public String request(Bundle parameters) throws MalformedURLException,
			IOException {
		if (!parameters.containsKey("method")) {
			throw new IllegalArgumentException("API method must be specified. "
					+ "(parameters must contain key \"method\" and value). See"
					+ " http://developers.facebook.com/docs/reference/rest/");
		}
		return request(null, parameters, "GET");
	}

	/**
	 * Make a request to the Facebook Graph API without any parameters.
	 * 
	 * See http://developers.facebook.com/docs/api
	 * 
	 * Note that this method blocks waiting for a network response, so do not
	 * call it in a UI thread.
	 * 
	 * @param graphPath
	 *            Path to resource in the Facebook graph, e.g., to fetch data
	 *            about the currently logged authenticated user, provide "me",
	 *            which will fetch http://graph.facebook.com/me
	 * @throws IOException
	 * @throws MalformedURLException
	 * @return JSON string representation of the response
	 */
	public String request(String graphPath) throws MalformedURLException,
			IOException {
		return request(graphPath, new Bundle(), "GET");
	}

	/**
	 * Make a request to the Facebook Graph API with the given string parameters
	 * using an HTTP GET (default method).
	 * 
	 * See http://developers.facebook.com/docs/api
	 * 
	 * Note that this method blocks waiting for a network response, so do not
	 * call it in a UI thread.
	 * 
	 * @param graphPath
	 *            Path to resource in the Facebook graph, e.g., to fetch data
	 *            about the currently logged authenticated user, provide "me",
	 *            which will fetch http://graph.facebook.com/me
	 * @param parameters
	 *            key-value string parameters, e.g. the path "search" with
	 *            parameters "q" : "facebook" would produce a query for the
	 *            following graph resource:
	 *            https://graph.facebook.com/search?q=facebook
	 * @throws IOException
	 * @throws MalformedURLException
	 * @return JSON string representation of the response
	 */
	public String request(String graphPath, Bundle parameters)
			throws MalformedURLException, IOException {
		return request(graphPath, parameters, "GET");
	}

	/**
	 * Synchronously make a request to the Facebook Graph API with the given
	 * HTTP method and string parameters. Note that binary data parameters (e.g.
	 * pictures) are not yet supported by this helper function.
	 * 
	 * See http://developers.facebook.com/docs/api
	 * 
	 * Note that this method blocks waiting for a network response, so do not
	 * call it in a UI thread.
	 * 
	 * @param graphPath
	 *            Path to resource in the Facebook graph, e.g., to fetch data
	 *            about the currently logged authenticated user, provide "me",
	 *            which will fetch http://graph.facebook.com/me
	 * @param params
	 *            Key-value string parameters, e.g. the path "search" with
	 *            parameters {"q" : "facebook"} would produce a query for the
	 *            following graph resource:
	 *            https://graph.facebook.com/search?q=facebook
	 * @param httpMethod
	 *            http verb, e.g. "GET", "POST", "DELETE"
	 * @throws IOException
	 * @throws MalformedURLException
	 * @return JSON string representation of the response
	 */
	public String request(String graphPath, Bundle params, String httpMethod)
			throws FileNotFoundException, MalformedURLException, IOException {
		params.putString("format", "json");
		if (isSessionValid()) {
			params.putString(TOKEN, getAccessToken());
		}
		String url = (graphPath != null) ? GRAPH_BASE_URL + graphPath
				: RESTSERVER_URL;
		return Util.openUrl(url, httpMethod, params);
	}

	/**
	 * Generate a UI dialog for the request action in the given Android context.
	 * 
	 * Note that this method is asynchronous and the callback will be invoked in
	 * the original calling thread (not in a background thread).
	 * 
	 * @param context
	 *            The Android context in which we will generate this dialog.
	 * @param action
	 *            String representation of the desired method: e.g. "login",
	 *            "stream.publish", ...
	 * @param listener
	 *            Callback interface to notify the application when the dialog
	 *            has completed.
	 */
	public void dialog(Context context, String action, DialogListener listener) {
		dialog(context, action, new Bundle(), listener);
	}

	/**
	 * Generate a UI dialog for the request action in the given Android context
	 * with the provided parameters.
	 * 
	 * Note that this method is asynchronous and the callback will be invoked in
	 * the original calling thread (not in a background thread).
	 * 
	 * @param context
	 *            The Android context in which we will generate this dialog.
	 * @param action
	 *            String representation of the desired method: e.g. "feed" ...
	 * @param parameters
	 *            String key-value pairs to be passed as URL parameters.
	 * @param listener
	 *            Callback interface to notify the application when the dialog
	 *            has completed.
	 */
	public void dialog(Context context, String action, Bundle parameters,
			final DialogListener listener) {

		String endpoint = DIALOG_BASE_URL + action;
		parameters.putString("display", "touch");
		parameters.putString("redirect_uri", REDIRECT_URI);

		if (action.equals(LOGIN)) {
			parameters.putString("type", "user_agent");
			parameters.putString("client_id", mAppId);
		} else {
			parameters.putString("app_id", mAppId);
		}

		if (isSessionValid()) {
			parameters.putString(TOKEN, getAccessToken());
		}
		String url = endpoint + "?" + Util.encodeUrl(parameters);
		if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) {
			Util.showAlert(context, "Error",
					"Application requires permission to access the Internet");
		} else {
			new FbDialog(context, url, listener).show();
		}
	}

	/**
	 * @return boolean - whether this object has an non-expired session token
	 */
	public boolean isSessionValid() {
		return (getAccessToken() != null)
				&& ((getAccessExpires() == 0) || (System.currentTimeMillis() < getAccessExpires()));
	}

	/**
	 * Retrieve the OAuth 2.0 access token for API access: treat with care.
	 * Returns null if no session exists.
	 * 
	 * @return String - access token
	 */
	public String getAccessToken() {
		return mAccessToken;
	}

	/**
	 * Retrieve the current session's expiration time (in milliseconds since
	 * Unix epoch), or 0 if the session doesn't expire or doesn't exist.
	 * 
	 * @return long - session expiration time
	 */
	public long getAccessExpires() {
		return mAccessExpires;
	}

	/**
	 * Set the OAuth 2.0 access token for API access.
	 * 
	 * @param token
	 *            - access token
	 */
	public void setAccessToken(String token) {
		mAccessToken = token;
		mLastAccessUpdate = System.currentTimeMillis();
	}

	/**
	 * Set the current session's expiration time (in milliseconds since Unix
	 * epoch), or 0 if the session doesn't expire.
	 * 
	 * @param time
	 *            - timestamp in milliseconds
	 */
	public void setAccessExpires(long time) {
		mAccessExpires = time;
	}

	/**
	 * Set the current session's duration (in seconds since Unix epoch), or "0"
	 * if session doesn't expire.
	 * 
	 * @param expiresIn
	 *            - duration in seconds (or 0 if the session doesn't expire)
	 */
	public void setAccessExpiresIn(String expiresIn) {
		if (expiresIn != null) {
			long expires = expiresIn.equals("0") ? 0 : System
					.currentTimeMillis() + Long.parseLong(expiresIn) * 1000L;
			setAccessExpires(expires);
		}
	}

	public String getAppId() {
		return mAppId;
	}

	public void setAppId(String appId) {
		mAppId = appId;
	}

	/**
	 * Callback interface for dialog requests.
	 * 
	 */
	public static interface DialogListener {

		/**
		 * Called when a dialog completes.
		 * 
		 * Executed by the thread that initiated the dialog.
		 * 
		 * @param values
		 *            Key-value string pairs extracted from the response.
		 */
		public void onComplete(Bundle values);

		/**
		 * Called when a Facebook responds to a dialog with an error.
		 * 
		 * Executed by the thread that initiated the dialog.
		 * 
		 */
		public void onFacebookError(FacebookError e);

		/**
		 * Called when a dialog has an error.
		 * 
		 * Executed by the thread that initiated the dialog.
		 * 
		 */
		public void onError(DialogError e);

		/**
		 * Called when a dialog is canceled by the user.
		 * 
		 * Executed by the thread that initiated the dialog.
		 * 
		 */
		public void onCancel();

	}

	/**
	 * Callback interface for service requests.
	 */
	public static interface ServiceListener {

		/**
		 * Called when a service request completes.
		 * 
		 * @param values
		 *            Key-value string pairs extracted from the response.
		 */
		public void onComplete(Bundle values);

		/**
		 * Called when a Facebook server responds to the request with an error.
		 */
		public void onFacebookError(FacebookError e);

		/**
		 * Called when a Facebook Service responds to the request with an error.
		 */
		public void onError(Error e);

	}

	public static final String FB_APP_SIGNATURE = "30820268308201d102044a9c4610300d06092a864886f70d0101040500307a310"
			+ "b3009060355040613025553310b30090603550408130243413112301006035504"
			+ "07130950616c6f20416c746f31183016060355040a130f46616365626f6f6b204"
			+ "d6f62696c653111300f060355040b130846616365626f6f6b311d301b06035504"
			+ "03131446616365626f6f6b20436f72706f726174696f6e3020170d30393038333"
			+ "13231353231365a180f32303530303932353231353231365a307a310b30090603"
			+ "55040613025553310b30090603550408130243413112301006035504071309506"
			+ "16c6f20416c746f31183016060355040a130f46616365626f6f6b204d6f62696c"
			+ "653111300f060355040b130846616365626f6f6b311d301b06035504031314466"
			+ "16365626f6f6b20436f72706f726174696f6e30819f300d06092a864886f70d01"
			+ "0101050003818d0030818902818100c207d51df8eb8c97d93ba0c8c1002c928fa"
			+ "b00dc1b42fca5e66e99cc3023ed2d214d822bc59e8e35ddcf5f44c7ae8ade50d7"
			+ "e0c434f500e6c131f4a2834f987fc46406115de2018ebbb0d5a3c261bd97581cc"
			+ "fef76afc7135a6d59e8855ecd7eacc8f8737e794c60a761c536b72b11fac8e603"
			+ "f5da1a2d54aa103b8a13c0dbc10203010001300d06092a864886f70d010104050"
			+ "0038181005ee9be8bcbb250648d3b741290a82a1c9dc2e76a0af2f2228f1d9f9c"
			+ "4007529c446a70175c5a900d5141812866db46be6559e2141616483998211f4a6"
			+ "73149fb2232a10d247663b26a9031e15f84bc1c74d141ff98a02d76f85b2c8ab2"
			+ "571b6469b232d8e768a7f7ca04f7abe4a775615916c07940656b58717457b42bd"
			+ "928a2";

}